package com.gc.ip;

public class IPUtils
{
	// Constant definitions of IP TYPE
	public final static int IP_TYPE_NONE = 0;
	public final static int IP_TYPE_V4 = 0x01;
	public final static int IP_TYPE_V6 = 0x02;

	// IP_TYPE_ALL includes all the ip types
	public final static int IP_TYPE_ALL = (IP_TYPE_V4 | IP_TYPE_V6);

	// Byte number of IPv4
	public final static int IPV4_BYTE_SIZE = 4;
	// Byte number of IPv6
	public final static int IPV6_BYTE_SIZE = 16;
	// Byte number of type INT16
	private final static int INT16_BYTE_SIZE = 2;

	/*
	 * Converts IPv4 address in its textual presentation form into its numeric
	 * binary form.
	 * 
	 * @param src a String representing an IPv4 address in standard format
	 * 
	 * @return a byte array representing the IPv4 numeric address
	 */
	public static byte[] toIPv4Bytes(String src)
	{
		// Empty input
		if(src == null || src.isEmpty())
		{
			return null;
		}

		byte[] res = new byte[IPV4_BYTE_SIZE];
		String[] s = src.split("\\.", -1);

		// Not 4 parts
		if(s.length != IPV4_BYTE_SIZE)
		{
			return null;
		}

		int val = 0;
		try
		{
			/*
			 * When four parts are specified, each is interpreted as a byte of
			 * data and assigned, from left to right, to the four bytes of an
			 * IPv4 address.
			 */
			for(int i = 0; i < IPV4_BYTE_SIZE; i++)
			{
				val = Integer.parseInt(s[i]);
				if(val < 0 || val > 0xff)
				{
					return null;
				}
				res[i] = (byte) (val & 0xff);
			}
		}
		catch(NumberFormatException e)
		{
			return null;
		}
		return res;
	}

	/*
	 * Convert IPv6 presentation level address to network order binary form.
	 * 
	 * @param src a String representing an IPv6 address in textual format
	 * 
	 * @return a byte array representing the IPv6 numeric address
	 */
	public static byte[] toIPv6Bytes(String src)
	{
		// Shortest valid string is "::", hence at least 2 chars
		if(src == null || src.length() < 2)
		{
			return null;
		}

		String[] s = src.split("::", -1);

		byte[] ret = null;

		// Found zero ::
		if(s.length == 1)
		{
			ret = toBytes(src);
			if(ret == null || ret.length != IPV6_BYTE_SIZE)
			{
				return null;
			}
		}
		// Found one ::
		else if(s.length == 2)
		{
			// First part
			byte[] head = toBytes(s[0]);
			if(head == null)
			{
				return null;
			}

			// Second part
			byte[] tail = toBytes(s[1]);
			if(tail == null)
			{
				return null;
			}

			// Too longer than 16 bytes
			if(head.length + tail.length > IPV6_BYTE_SIZE)
			{
				return null;
			}

			// Merge the two parts together
			ret = new byte[IPV6_BYTE_SIZE];

			// merge the first part
			System.arraycopy(head, 0, ret, 0, head.length);

			// merge the second part
			int tailOffset = IPV6_BYTE_SIZE - tail.length;
			System.arraycopy(tail, 0, ret, tailOffset, tail.length);

			// set the interval to zero(s)
			for(int i = head.length; i < tailOffset; i++)
			{
				ret[i] = 0;
			}
		}

		return ret;
	}

	/**
	 * Try to convert a text to network order binary form.
	 * 
	 * @param src
	 * @param ipType
	 *            indicate the IP type
	 * @return
	 */
	public static byte[] toIPBytes(String src, int ipType)
	{
		byte[] addr = null;

		if((ipType & IP_TYPE_V4) != 0)
		{
			addr = toIPv4Bytes(src);
			if(addr != null)
			{
				return addr;
			}
		}

		if((ipType & IP_TYPE_V6) != 0)
		{
			addr = toIPv6Bytes(src);
			if(addr != null)
			{
				return addr;
			}
		}

		return null;
	}

	/**
	 * Try to convert a IPv4 or IPv6 text to network order binary form.
	 * 
	 * @param src
	 * @return
	 */
	public static byte[] toIPBytes(String src)
	{
		return toIPBytes(src, IP_TYPE_ALL);
	}

	/**
	 * Helper function to convert a string with ":" separated to bytes array
	 * 
	 * @param src
	 * @return
	 */
	private static byte[] toBytes(String src)
	{
		if(src.isEmpty())
		{
			return new byte[0];
		}
		// There should not be : at head or tail
		if(src.startsWith(":") || src.endsWith(":"))
		{
			return null;
		}

		String[] s = src.split(":", -1);
		if(s.length > (IPV6_BYTE_SIZE / INT16_BYTE_SIZE))
		{
			return null;
		}

		byte[] ret = new byte[s.length * 2];
		int val = 0;

		try
		{
			for(int i = 0; i < s.length; i++)
			{
				val = Integer.parseInt(s[i], 16);
				if(val < 0 || val > 0xffff)
				{
					return null;
				}

				ret[i * 2] = (byte) ((val >> 8) & 0xff);
				ret[i * 2 + 1] = (byte) (val & 0xff);
			}
		}
		catch(NumberFormatException e)
		{
			return null;
		}

		return ret;
	}

	/**
	 * @param src
	 *            a String representing an IPv4 address in textual format
	 * @return a boolean indicating whether src is an IPv4 literal address
	 */
	public static boolean isIPv4Address(String src)
	{
		return toIPv4Bytes(src) != null;
	}

	/**
	 * @param src
	 *            a String representing an IPv6 address in textual format
	 * @return a boolean indicating whether src is an IPv6 literal address
	 */
	public static boolean isIPv6Address(String src)
	{
		return toIPv6Bytes(src) != null;
	}

	/**
	 * Converts IPv4 binary address into a string suitable for presentation.
	 * 
	 * @param addr
	 *            a byte array representing an IPv4 numeric address
	 * @return a String representing the IPv4 address in textual representation
	 *         format
	 */
	public static String toIPv4String(byte[] addr)
	{
		if(addr == null || addr.length != IPV4_BYTE_SIZE)
		{
			return null;
		}
		StringBuilder sb = new StringBuilder(15);
		for(int i = 0; i < IPV4_BYTE_SIZE; i++)
		{
			if(i > 0)
			{
				sb.append('.');
			}
			sb.append(addr[i] & 0xff);
		}
		return sb.toString();
	}

	/**
	 * Convert IPv6 binary address into presentation (printable) format.
	 * 
	 * @param addr
	 *            a byte array representing the IPv6 numeric address
	 * @return a String representing an IPv6 address in textual representation
	 *         format
	 */
	public static String toIPv6String(byte[] addr)
	{
		if(addr == null || addr.length != IPV6_BYTE_SIZE)
		{
			return null;
		}
		StringBuilder sb = new StringBuilder();

		int val = 0;
		for(int i = 0; i < (IPV6_BYTE_SIZE / INT16_BYTE_SIZE); i++)
		{
			if(i > 0)
			{
				sb.append(':');
			}
			val = ((addr[i * 2] << 8) & 0xff00) | (addr[(i * 2) + 1] & 0xff);
			sb.append(Integer.toHexString(val));
		}
		return sb.toString();
	}

	/**
	 * Try to convert a IPv4 or IPv6 binary address into presentation
	 * (printable) format.
	 * 
	 * @param addr
	 * @return
	 */
	public static String toIPString(byte[] addr)
	{
		String s = toIPv4String(addr);
		if(s == null)
		{
			s = toIPv6String(addr);
		}
		return s;
	}

	/**
	 * Convert prefix length into mask byte array format. If the mask is invalid
	 * (out of range), -1 will be returned.
	 * 
	 * @param prefixLength
	 * @param byteCount
	 *            how many bytes of the prefix length
	 * @return mask byte array
	 */
	public static byte[] prefixLengthToMaskBytes(int prefixLength, int byteCount)
	{
		// out of range
		if(prefixLength < 0 || prefixLength > byteCount * 8)
		{
			return null;
		}

		// convert the prefix length into byte array
		byte[] addr = new byte[byteCount];

		// count of 0xFF
		int div = prefixLength / 8;

		// 1 bits count
		int rem = prefixLength % 8;

		int i = 0;
		for(i = 0; i < div; i++)
		{
			addr[i] = (byte) 0xFF;
		}
		// convert length to the byte format
		if(rem != 0)
		{
			addr[i++] = (byte) ((0xFF << (8 - rem)) & 0xFF);
		}
		// all the left is zero
		for(; i < byteCount; i++)
		{
			addr[i] = 0;
		}

		return addr;
	}

	/**
	 * Convert mask byte array into prefix length
	 * 
	 * @param addr
	 *            mask byte array
	 * @param byteCount
	 *            how many bytes of the target prefix length
	 * @return prefix length
	 */
	public static int maskBytesToPrefixLength(byte[] addr, int byteCount)
	{
		if(addr == null || addr.length < byteCount)
		{
			return -1;
		}

		// find the first non 0xFF byte index
		int x = -1;
		for(int i = 0; i < byteCount; i++)
		{
			if(addr[i] != (byte) 0xFF)
			{
				x = i;
				break;
			}
		}

		// all the bytes in the array are equal to 0xFF
		if(x < 0)
		{
			return byteCount * 8;
		}

		// make sure the left bytes are equal to 0
		for(int i = x + 1; i < byteCount; i++)
		{
			if(addr[i] != 0)
			{
				return -1;
			}
		}

		// the 0xFF count is x
		int prefixLength = (x << 3); // = x*8

		// addr[x] != 0 and addr[x] != 0xFF
		if(addr[x] != 0)
		{
			// Check if the mask is valid, a valid mask must meet:
			// all the 1 bits is in the highest bits
			// all the 0 bits is in the lowest bits
			// e.g. 0xF8 is valid, 0xF3 is invalid
			int ntz = Integer.numberOfTrailingZeros(addr[x]);
			if(addr[x] != (byte) ((0xFF << ntz)))
			{
				return -1;
			}

			// add the number of 1 bits
			prefixLength += (8 - ntz);
		}

		return prefixLength;
	}

	/**
	 * Convert IPv4 prefix length into mask string format. If the prefixLength
	 * is out of range (0-32), null will be returned.
	 * 
	 * @param prefixLength
	 * @return IPv4 mask string
	 */
	public static String toIPv4MaskString(int prefixLength)
	{
		byte[] addr = prefixLengthToMaskBytes(prefixLength, IPV4_BYTE_SIZE);

		return toIPv4String(addr);
	}

	/**
	 * Converts IPv4 mask into prefix length. If the mask is invalid, -1 will be
	 * returned.
	 * 
	 * @param mask
	 *            a String representing an IPv4 mask
	 * @return prefix length
	 */
	public static int toIPv4PrefixLength(String mask)
	{
		// convert the mask string to byte array
		byte[] addr = toIPv4Bytes(mask);

		return maskBytesToPrefixLength(addr, IPV4_BYTE_SIZE);
	}

	/**
	 * Convert IPv6 prefix length into mask string format. If the prefixLength
	 * is out of range (0-128), null will be returned.
	 * 
	 * @param prefixLength
	 * @return IPv6 mask string
	 */
	public static String toIPv6MaskString(int prefixLength)
	{
		byte[] addr = prefixLengthToMaskBytes(prefixLength, IPV6_BYTE_SIZE);

		return toIPv6String(addr);
	}

	/**
	 * Converts IPv6 mask into prefix length. If the mask is invalid, -1 will be
	 * returned.
	 * 
	 * @param mask
	 *            a String representing an IPv6 mask
	 * @return prefix length
	 */
	public static int toIPv6PrefixLength(String mask)
	{
		// convert the mask string to byte array
		byte[] addr = toIPv6Bytes(mask);

		return maskBytesToPrefixLength(addr, IPV6_BYTE_SIZE);
	}

	/**
	 * Unit Test method
	 * 
	 * @param args
	 */
	public static void main(String[] args)
	{
		String[] sources = { "2001:0db8:85a3:08d3:1319:8a2e:0370:7344",
			"2001:0db8:85a3:08d3:1319:8a2e:0370:", "2001:0db8:85a3:08d3:1319:8a2e:0370",
			"2001:DB8:2de::f13", "2001:0DB8::1428:57ab", "2001::25de::cade",
			"2001:0DB8:::1428:57ab", "2001:0DB8::::1428:57ab", "::1", "1::", "1.2.3.4",
			"1.343.2.6", "1.3", "a.2.5.3", "::1:2" };

		String src = null, dst = null;

		// Test IP convert
		System.out.println("Test IP Converting...\n");
		for(int i = 0; i < sources.length; i++)
		{
			src = sources[i];
			dst = null;
			byte[] b = toIPBytes(src, IP_TYPE_ALL);

			if(b != null)
			{
				dst = toIPString(b);
			}

			System.out.printf("[%02d]: source=%s, dst=%s\n", i, src, dst);
		}
		System.out.println();

		int x = 0;
		String mask = null;

		// Test IPv4 mask convert
		System.out.println("Test IPv4 mask Converting...\n");
		for(int i = -2; i <= 35; i++)
		{
			mask = toIPv4MaskString(i);
			x = toIPv4PrefixLength(mask);
			System.out.printf("[%s]: i=%3d, x=%3d, mask=%s\n", ((i == x) ? "Y" : "N"), i, x, mask);
		}
		System.out.println();

		// Test IPv6 mask convert
		System.out.println("Test IPv6 mask Converting...\n");
		for(int i = -2; i <= 130; i++)
		{
			mask = toIPv6MaskString(i);
			x = toIPv6PrefixLength(mask);
			System.out.printf("[%s]: i=%3d, x=%3d, mask=%s\n", ((i == x) ? "Y" : "N"), i, x, mask);
		}
		System.out.println();

		// Invalid mask input test
		System.out.println("Invalid IPv4 mask Converting...\n");
		for(int i = 0; i < sources.length; i++)
		{
			x = toIPv4PrefixLength(sources[i]);
			mask = toIPv6MaskString(x);
			System.out.printf("[%s]: x=%3d, mask=%s, source=%s\n", ((sources[i].equals(mask)) ? "Y"
				: "N"), x, mask, sources[i]);
		}
		System.out.println();

		System.out.println("Invalid IPv6 mask Converting...\n");
		for(int i = 0; i < sources.length; i++)
		{
			x = toIPv4PrefixLength(sources[i]);
			mask = toIPv6MaskString(x);
			System.out.printf("[%s]: x=%3d, mask=%s, source=%s\n", ((sources[i].equals(mask)) ? "Y"
				: "N"), x, mask, sources[i]);
		}
		System.out.println();
	}
}
